Conversation
| error: string | null; | ||
| }; | ||
|
|
||
| export function useCustomFetch<TData>( |
There was a problem hiding this comment.
전반적으로 코드 잘 보았습니다. 몇 가지 부분만 개선되면 더 좋은 코드가 될 것 같습니다!
*현재 문제
훅이 “의존성 변화”로만 다시 호출됩니다. 버튼으로 새로 고침, 무한 스크롤, 필터 적용 직후 재조회 같은 수동 재요청이 불가합니다. 네트워크가 느릴 때 “먼저 보낸 요청”이 “나중에 보낸 요청”보다 늦게 도착하면, 낡은 응답이 최신 화면을 덮어쓰는 경쟁 상태가 발생할 수 있습니다.
*개선안
훅 내부의 요청 함수를 refetch()로 노출해 사용자가 원하는 시점에 재요청할 수 있게 합니다. 요청마다 고유 id를 부여해 가장 최신 요청만 상태에 반영하도록 합니다.
axios.isCancel/ERR_CANCELED는 에러로 취급하지 않고 조용히 무시합니다.
*기대 효과
검색/필터/페이지네이션/재시도 버튼 등 다양한 UI에서 동일 훅 재사용성이 높아지고, 느린 이전 응답이 최신 화면을 덮어쓰는 버그를 예방합니다.
*테스트 포인트
필터 값을 빠르게 여러 번 바꿔도 최종 선택 값의 결과만 렌더링되는지 확인 “새로고침” 버튼을 눌렀을 때 loading 플래그가 정상 동작하고 에러/성공 후 상태가 일관적인지 확인합니다.
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
import axios from "axios";
import { tmdb } from "../api/tmdb";
type UseCustomFetchOptions = {
params?: Record<string, unknown>;
enabled?: boolean;
/** 외부 의존성(검색어, 필터 등) 변경 시 자동 재요청 /
dependencies?: unknown[];
/* 초기 표시용 데이터가 있으면 첫 로딩 스피너를 숨깁니다. */
initialData?: TData | null;
};
type UseCustomFetchReturn = {
data: TData | null;
loading: boolean;
error: string | null;
/** 수동 재요청 (새로고침/재시도 버튼 등에서 사용) */
refetch: () => Promise;
};
export function useCustomFetch<TData = unknown>(
endpoint: string | null,
options: UseCustomFetchOptions = {}
): UseCustomFetchReturn {
const {
params,
enabled = true,
dependencies = [],
initialData = null,
} = options;
const [data, setData] = useState<TData | null>(initialData);
const [loading, setLoading] = useState(
() => Boolean(enabled && endpoint && !initialData)
);
const [error, setError] = useState<string | null>(null);
// 최신 요청만 상태에 반영하기 위한 요청 id 및 취소 컨트롤러 보관
const requestIdRef = useRef(0);
const controllerRef = useRef<AbortController | null>(null);
// params를 안정적으로 비교하기 위한 키 (key 순서 정렬 후 JSON 직렬화)
const paramsKey = useMemo(() => {
if (!params) return "";
try {
const entries = Object.entries(params).sort(([a], [b]) =>
a > b ? 1 : a < b ? -1 : 0
);
return JSON.stringify(entries);
} catch {
// 실패시 예측 가능하게 고정값 반환(불필요한 재요청 방지)
return "";
}
}, [params]);
const refetch = useCallback(async () => {
if (!enabled || !endpoint) return;
// 이전 요청 중단
if (controllerRef.current) controllerRef.current.abort();
const controller = new AbortController();
controllerRef.current = controller;
// 이 요청의 고유 id
const myId = ++requestIdRef.current;
setLoading(true);
setError(null);
try {
const res = await tmdb.get<TData>(endpoint, {
params,
signal: controller.signal,
});
// 최신 요청만 반영
if (myId === requestIdRef.current) {
setData(res.data);
}
} catch (err: any) {
// 취소된 요청은 무시
const isCanceled =
axios.isCancel?.(err) ||
err?.code === "ERR_CANCELED" ||
err?.name === "CanceledError";
if (isCanceled) return;
if (myId === requestIdRef.current) {
const message =
err instanceof Error
? err.message
: "데이터를 불러오는 중 문제가 발생했습니다.";
setError(message);
}
} finally {
if (myId === requestIdRef.current) {
setLoading(false);
}
}
}, [enabled, endpoint, paramsKey]);
// 초기 로딩 및 의존성 변경 시 자동 재요청
useEffect(() => {
if (!enabled || !endpoint) {
setLoading(false);
return;
}
void refetch();
// 언마운트/다음 요청 전 현재 요청 취소
return () => {
if (controllerRef.current) controllerRef.current.abort();
};
// dependencies는 외부에서 명시적으로 전달받은 값만 추가
}, [refetch, ...dependencies]);
return { data, loading, error, refetch };
}
No description provided.